package cn.dev33.satoken.oauth2.logic;

import cn.dev33.satoken.LoginUtil;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.context.model.SaResponse;
import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.oauth2.PasswordLogin;
import cn.dev33.satoken.oauth2.SaOAuth2Manager;
import cn.dev33.satoken.oauth2.config.SaOAuth2Config;
import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception;
import cn.dev33.satoken.oauth2.logic.SaOAuth2Consts.Api;
import cn.dev33.satoken.oauth2.logic.SaOAuth2Consts.GrantType;
import cn.dev33.satoken.oauth2.logic.SaOAuth2Consts.Param;
import cn.dev33.satoken.oauth2.logic.SaOAuth2Consts.ResponseType;
import cn.dev33.satoken.oauth2.model.AccessTokenModel;
import cn.dev33.satoken.oauth2.model.CodeModel;
import cn.dev33.satoken.oauth2.model.RequestAuthModel;
import cn.dev33.satoken.oauth2.model.SaClientModel;
import cn.dev33.satoken.stp.StpUtil;
import com.csap.framework.extension.result.ResultParam;
import com.csap.framework.extension.result.ResultUtil;
import com.csap.framework.util.Assert;
import com.csap.mybatisplus.core.toolkit.StringPool;
import org.apache.commons.collections.CollectionUtils;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * Sa-Token-OAuth2 请求处理类封装
 *
 * @author kong
 */
public class SaOAuth2Handle {
    /**
     * 处理Server端请求， 路由分发
     *
     * @return 处理结果
     */
    public static Object serverRequest() {

        // 获取变量
        SaRequest req = SaHolder.getRequest();
        SaResponse res = SaHolder.getResponse();
        SaOAuth2Config cfg = SaOAuth2Manager.getConfig();
        // ------------------ 路由分发 ------------------

        // 模式一：Code授权码
        if (req.isPath(Api.authorize) && req.isParam(Param.response_type, ResponseType.code) && cfg.isCode) {
            return authorize(req, res, cfg);
        }

        // Code授权码 获取 Access-Token
        if (req.isPath(Api.token) && req.isParam(Param.grant_type, GrantType.authorization_code)) {
            return token(req, res, cfg);
        }

        // Refresh-Token 刷新 Access-Token
        if (req.isPath(Api.refresh) && req.isParam(Param.grant_type, GrantType.refresh_token)) {
            return refreshToken(req);
        }

        // doLogin 登录接口
        if (req.isPath(Api.doLogin)) {
            return doLogin(req, res, cfg);
        }

        // doConfirm 确认授权接口
        if (req.isPath(Api.doConfirm)) {
            return doConfirm(req);
        }

        // 模式二：隐藏式
        if (req.isPath(Api.authorize) && req.isParam(Param.response_type, ResponseType.token) && cfg.isImplicit) {
            return authorize(req, res, cfg);
        }

        // 模式三：密码式
        if (req.isPath(Api.token) && req.isParam(Param.grant_type, GrantType.password) && cfg.isPassword) {
            return password(req, res, cfg);
        }

        // 模式四：凭证式
        if (req.isPath(Api.client_token) && req.isParam(Param.grant_type, GrantType.client_credentials) && cfg.isClient) {
            return clientToken(req, res, cfg);
        }

        // 默认返回
        return SaOAuth2Consts.NOT_HANDLE;
    }

    /**
     * 模式一：Code授权码 / 模式二：隐藏式
     *
     * @param req 请求对象
     * @param res 响应对象
     * @param cfg 配置对象
     * @return 处理结果
     */
    public static Object authorize(SaRequest req, SaResponse res, SaOAuth2Config cfg) {

        // 1、如果尚未登录, 则先去登录
        if (!StpUtil.isLogin()) {
            return cfg.notLoginView.get();
        }
        // 2、构建请求Model
        RequestAuthModel ra = SaOAuth2Util.generateRequestAuth(req, StpUtil.getLoginId());
        ra.setPlatform(StpUtil.getSession().getPlatform());
        // 3、校验：重定向域名是否合法
        SaOAuth2Util.checkRightUrl(ra.getClientId(), ra.getRedirectUri());
        // 4、校验：此次申请的Scope，该Client是否已经签约
        SaOAuth2Util.checkContract(ra.getClientId(), ra.getScope());
        // 5、判断：如果此次申请的Scope，该用户尚未授权，则转到授权页面
        boolean isGrant = SaOAuth2Util.isGrant(ra.loginId, ra.clientId, ra.scope);
        if (!isGrant) {
            return cfg.confirmView.apply(ra.clientId, ra.scope);
        }

        // 6、判断授权类型
        // 如果是 授权码式，则：开始重定向授权，下放code
        if (ResponseType.code.equals(ra.responseType)) {
            CodeModel codeModel = SaOAuth2Util.generateCode(ra);
            String redirectUri = SaOAuth2Util.buildRedirectUri(ra.redirectUri, codeModel.code, ra.state);
            return res.redirect(redirectUri);
        }
        // 如果是 隐藏式，则：开始重定向授权，下放 token
        if (ResponseType.token.equals(ra.responseType)) {
            AccessTokenModel at = SaOAuth2Util.generateAccessToken(ra, false);
            String redirectUri = SaOAuth2Util.buildImplicitRedirectUri(ra.redirectUri, at.getAccessToken(), ra.state);
            return res.redirect(redirectUri);
        }

        // 默认返回
        throw new SaOAuth2Exception("无效response_type: " + ra.responseType);
    }

    /**
     * Code授权码 获取 Access-Token
     *
     * @param req 请求对象
     * @param res 响应对象
     * @param cfg 配置对象
     * @return 处理结果
     */
    public static Object token(SaRequest req, SaResponse res, SaOAuth2Config cfg) {
        // 获取参数
        String code = req.getParamNotNull(Param.code);
        String clientId = req.getParamNotNull(Param.client_id);
        String clientSecret = req.getParamNotNull(Param.client_secret);
        String redirectUri = req.getParam(Param.redirect_uri);
        String platform = req.getParam(Param.platform);

        // 校验参数
        SaOAuth2Util.checkGainTokenParam(code, clientId, clientSecret, redirectUri, platform);

        // 构建 Access-Token

        // 返回
        return ResultUtil.success(SaOAuth2Util.generateAccessToken(code));
    }

    /**
     * Refresh-Token 刷新 Access-Token
     *
     * @param req 请求对象
     * @return 处理结果
     */
    public static Object refreshToken(SaRequest req) {
        // 获取参数
        String clientId = req.getParamNotNull(Param.client_id);
        String clientSecret = req.getParamNotNull(Param.client_secret);
        String refreshToken = req.getParamNotNull(Param.refresh_token);
        String platform = req.getParamNotNull(Param.platform);
        // 校验参数
        SaOAuth2Util.checkRefreshTokenParam(clientId, clientSecret, refreshToken, platform);
        // 获取新Token返回
        return ResultUtil.success(SaOAuth2Util.saOAuth2Template.refreshAccessToken(refreshToken));
    }

    /**
     * doLogin 登录接口
     *
     * @param req 请求对象
     * @param res 响应对象
     * @param cfg 配置对象
     * @return 处理结果
     */
    public static Object doLogin(SaRequest req, SaResponse res, SaOAuth2Config cfg) {
        return cfg.doLoginHandle.apply(req.getParamNotNull(Param.name), req.getParamNotNull("pwd"));
    }

    /**
     * doConfirm 确认授权接口
     *
     * @param req 请求对象
     * @return 处理结果
     */
    public static Object doConfirm(SaRequest req) {
        String clientId = req.getParamNotNull(Param.client_id);
        String scope = req.getParamNotNull(Param.scope);
        Object loginId = StpUtil.getLoginId();
        SaOAuth2Util.saveGrantScope(clientId, loginId, scope);
        return ResultUtil.success();
    }

    /**
     * 模式三：密码式
     *
     * @param req 请求对象
     * @param res 响应对象
     * @param cfg 项目配置
     * @return 结果
     */
    public static <T> T password(SaRequest req, SaResponse res, SaOAuth2Config cfg) {
        return (T) password(PasswordLogin.builder()
                .userName(req.getParamNotNull(Param.username))
                .password(req.getParamNotNull(Param.password))
                .clientId(req.getParamNotNull(Param.client_id))
                .deviceType(req.getParam(Param.deviceType, "default"))
                .platform(LoginUtil.platform())
                .loginType(LoginUtil.loginType())
                .scope(req.getParam(Param.scope, "")).build());
    }

    public static <T> T password(Map<String, String> map, SaRequest req, SaResponse res, SaOAuth2Config cfg) {
        return (T) password(PasswordLogin.builder()
                .userName(Assert.notNull(map.get(Param.username), "用户名不能为空"))
                .password(Assert.notEmpty(map.get(Param.password), "密码不能为空"))
                .clientId(Assert.notEmpty(map.get(Param.client_id), "客户端ID不能为空"))
                .deviceType(map.getOrDefault(Param.deviceType, "default"))
                .platform(LoginUtil.platform())
                .loginType(LoginUtil.loginType())
                .scope(map.getOrDefault(Param.scope, "")).build());
    }

    /**
     * 模式三：密码式
     *
     * @param passwordLogin 登录信息
     * @return 处理结果
     */
    public static ResultParam<AccessTokenModel> password(PasswordLogin passwordLogin) {
        // 2、校验client_id
        SaOAuth2Util.checkClientModel(passwordLogin.getClientId());
        // 3、防止因前端误传token造成逻辑干扰
        SaHolder.getStorage().set(StpUtil.stpLogic.splicingKeyJustCreatedSave(), "no-token");

        // 4、调用API 开始登录，如果没能成功登录，则直接退出
        ResultParam<AccessTokenModel> retObj = SaOAuth2Manager.getConfig().loginHandle.apply(passwordLogin);
        if (!LoginUtil.getInstance(passwordLogin.getLoginType()).isLogin()) {
            return retObj;
        }
        // 5、构建 ra对象
        return retObj;
    }

    /**
     * 模式四：凭证式
     *
     * @param req 请求对象
     * @param res 响应对象
     * @param cfg 配置对象
     * @return 处理结果
     */
    public static Object clientToken(SaRequest req, SaResponse res, SaOAuth2Config cfg) {
        // 获取参数
        String clientId = req.getParamNotNull(Param.client_id);
        String clientSecret = req.getParamNotNull(Param.client_secret);
        Assert.notNull(clientId, "clientId不能为空");
        Assert.notNull(clientSecret, "clientSecret不能为空");
        // 校验 ClientSecret
        SaClientModel saClientModel = SaOAuth2Util.checkSpClientSecret(clientId, clientSecret);
        String scope = req.getParamNotNull(Param.scope);
        List<String> collect = Arrays.stream(scope.split(StringPool.COMMA)).filter(i -> !saClientModel.getScope().contains(i)).collect(Collectors.toList());
        if (CollectionUtils.isNotEmpty(collect)) {
            throw new SaTokenException(String.format("作用域%s暂未签约", String.join(StringPool.COMMA, collect)));
        }
        return ResultUtil.success(SaOAuth2Util.generateClientToken(clientId, scope));
    }

}
